/**
 * @param {string[]} rules
 */
export function styleInject(rules) {
    if (!rules.length) {
        return;
    }

    const style = document.createElement("style");
    style.setAttribute("type", "text/css");
    document.head.appendChild(style);

    const sheet = style.sheet;
    if (!sheet) {
        return;
    }

    let pos = sheet.cssRules.length;
    for (let rule of rules) {
        style.sheet.insertRule(rule, pos++);
    }
}

/**
 * @param {string} selector
 * @param {boolean} value
 */
export function styleVisible(selector, value) {
    return `${selector} { content-visibility: ${value ? "visible": "hidden"}; }`
}

/**
 * @param {number} timeout
 */
export function pageReloadIn(timeout) {
    setTimeout(() => window.location.reload(), timeout);
}

/**
 * @param {HTMLElement} container
 */
function moreElem(container) {
    container.querySelectorAll(".more")
        .forEach((elem) => {
            if (!(elem instanceof HTMLElement)) {
                return;
            }

            elem.style.display = (elem.style.display === "")
                ? "inherit" : "";
        });
}

/**
 * @param {Event} event
 */
export function onMoreParent(event) {
    event.preventDefault();
    event.stopPropagation();

    const target = event.target;
    if (!(target instanceof HTMLElement)) {
        return;
    }

    const parent = target?.parentElement?.parentElement;
    if (parent) {
        moreElem(parent);
    }
}

/**
 * @param {Element | DocumentFragment} container
 */
export function moreParent(container) {
    for (let elem of container.querySelectorAll("button.button-more-parent")) {
        elem.addEventListener("click", onMoreParent);
    }
}

/**
 * @param {HTMLElement} elem
 */
export function lastMoreElem(elem) {
    if (elem.lastChild instanceof HTMLElement) {
        moreElem(elem.lastChild)
    }
}

/**
 * @param {HTMLElement} elem
 */
function menuToggle(elem) {
    elem.classList.toggle("active");
}

/**
 * @param {HTMLElement} elem
 */
function menuHide(elem) {
    elem.classList.remove("active");
}

/**
 * @param {Event} event
 * @returns {any}
 */
export function onMenuLinkClick(event) {
    event.preventDefault();

    const target = event.target;
    if (!(target instanceof HTMLElement)) {
        return;
    }

    if (target.parentElement) {
        menuToggle(target.parentElement);
    }
}

/**
 * @param {HTMLElement} elem
 */
export function showPanel(elem) {
    if (elem.style.display !== "revert") {
        for (const panel of document.getElementsByClassName("panel")) {
            if (!(panel instanceof HTMLElement)) {
                continue;
            }

            panel.style.display = "none";
        }

        elem.style.display = "revert";
    }

    const layout = document.getElementById("layout")
    if (layout) {
        menuHide(layout);
    }

    // TODO: sometimes, switching view causes us to scroll past
    // the header (e.g. emon ratios panel on small screen)
    // layout itself stays put, but the root element seems to scroll,
    // at least can be reproduced with Chrome
    if (document.documentElement) {
        document.documentElement.scrollTop = 0;
    }
}

/**
 * @param {HTMLElement} elem
 * @param {function(HTMLElement): void} callback
 */
export function findPanel(elem, callback) {
    const panel = elem.closest(".panel");
    if (!(panel instanceof HTMLElement)) {
        return;
    }

    callback(panel);
}

/**
 * @param {string} name
 */
export function showPanelByName(name) {
    // only a single panel is shown on the 'layout'
    const panel = document.getElementById(`panel-${name}`);
    if (!panel) {
        return;
    }

    showPanel(panel);
}

/**
 * @param {Event} event
 */
export function onPanelTargetClick(event) {
    event.preventDefault();

    const target = event.target;
    if (!(target instanceof HTMLElement)) {
        return;
    }

    const name = target.dataset["panel"];
    if (name) {
        showPanelByName(name);
    }

    panelTargetShowSelected(target);
}

/**
 * @param {HTMLElement} elem
 */
function panelTargetShowSelected(elem) {
    const root = elem.closest("#menu");
    if (!root) {
        return;
    }

    const parent = elem.parentElement;
    if (!parent) {
        return;
    }

    parent.classList.add("pure-menu-selected");

    /** @type {NodeListOf<HTMLAnchorElement>} */
    (root.querySelectorAll("#menu a[data-panel]"))
        .forEach((a) => {
            if (a.parentElement && a.parentElement !== parent) {
                a.parentElement.classList.remove("pure-menu-selected");
            }
        });
}

/**
 * @typedef {{hex?: boolean, lowercase?: boolean, numbers?: boolean, special?: boolean, uppercase?: boolean}} RandomStringOptions
 *
 * @param {number} length
 * @param {RandomStringOptions} options
 */
export function randomString(length, {hex = false, lowercase = true, numbers = true, special = false, uppercase = true} = {}) {
    let mask = "";
    if (lowercase || hex) { mask += "abcdef"; }
    if (lowercase) { mask += "ghijklmnopqrstuvwxyz"; }
    if (uppercase || hex) { mask += "ABCDEF"; }
    if (uppercase) { mask += "GHIJKLMNOPQRSTUVWXYZ"; }
    if (numbers || hex) { mask += "0123456789"; }
    if (special) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }

    const source = new Uint32Array(length);
    const result = new Array(length);

    window.crypto
        .getRandomValues(source)
        .forEach((value, i) => {
            result[i] = mask[value % mask.length];
        });

    return result.join("");
}

/**
 * @throws {Error}
 * @param {boolean} value
 * @param {string} message
 * @returns {asserts value}
 */
export function assert(value, message = "") {
    if (!value) {
        throw new Error(message ?? "assertion failed");
    }
}

/**
 * @template T
 * @param {T[]} values
 * @param {function(T): boolean} callback
 * @returns {number}
 */
export function count(values, callback) {
    return values.filter(callback).length;
}

/**
 * @param {string} value
 * @returns {string}
 */
export function capitalize(value) {
    return value === ""
        ? value
        : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
}

/**
 * @param {string} value
 * @returns {boolean}
 */
export function stringToBoolean(value) {
    return [
        "1",
        "y",
        "yes",
        "true",
        "on",
    ].includes(value.toLowerCase());
}
